import { createIdentity } from "actions/createIdentity"; import { subscribeToPublication } from "app/lish/subscribeToPublication"; import { drizzle } from "drizzle-orm/node-postgres"; import { cookies } from "next/headers"; import { redirect } from "next/navigation"; import { NextRequest, NextResponse } from "next/server"; import { createOauthClient } from "src/atproto-oauth"; import { setAuthToken } from "src/auth"; import { supabaseServerClient } from "supabase/serverClient"; import { URLSearchParams } from "url"; import { ActionAfterSignIn, parseActionFromSearchParam, } from "./afterSignInActions"; import { pool } from "supabase/pool"; type OauthRequestClientState = { redirect: string | null; action: ActionAfterSignIn | null; }; export async function GET( req: NextRequest, props: { params: Promise<{ route: string; handle?: string }> }, ) { const params = await props.params; let client = await createOauthClient(); switch (params.route) { case "metadata": return NextResponse.json(client.clientMetadata); case "jwks": return NextResponse.json(client.jwks); case "login": { const searchParams = req.nextUrl.searchParams; const handle = searchParams.get("handle") as string; // Put originating page here! let redirect = searchParams.get("redirect_url"); if (redirect) redirect = decodeURIComponent(redirect); let action = parseActionFromSearchParam(searchParams.get("action")); let state: OauthRequestClientState = { redirect, action }; // Revoke any pending authentication requests if the connection is closed (optional) const ac = new AbortController(); const url = await client.authorize(handle || "https://bsky.social", { scope: "atproto transition:generic transition:email", signal: ac.signal, state: JSON.stringify(state), }); return NextResponse.redirect(url); } case "callback": { const params = new URLSearchParams(req.url.split("?")[1]); let redirectPath = "/"; try { const { session, state } = await client.callback(params); let s: OauthRequestClientState = JSON.parse(state || "{}"); redirectPath = decodeURIComponent(s.redirect || "/"); let { data: identity } = await supabaseServerClient .from("identities") .select() .eq("atp_did", session.did) .single(); if (!identity) { let existingIdentity = (await cookies()).get("auth_token"); if (existingIdentity) { let data = await supabaseServerClient .from("email_auth_tokens") .select("*, identities(*)") .eq("id", existingIdentity.value) .single(); if (data.data?.identity && data.data.confirmed) await supabaseServerClient .from("identities") .update({ atp_did: session.did }) .eq("id", data.data.identity); return handleAction(s.action, redirectPath); } const client = await pool.connect(); const db = drizzle(client); identity = await createIdentity(db, { atp_did: session.did }); client.release(); } let { data: token } = await supabaseServerClient .from("email_auth_tokens") .insert({ identity: identity.id, confirmed: true, confirmation_code: "", }) .select() .single(); if (token) await setAuthToken(token.id); // Process successful authentication here console.log("authorize() was called with state:", state); console.log("User authenticated as:", session.did); return handleAction(s.action, redirectPath); } catch (e) { redirect(redirectPath); } } default: return NextResponse.json({ error: "Invalid route" }, { status: 404 }); } } const handleAction = async ( action: ActionAfterSignIn | null, redirectPath: string, ) => { let parsePath = decodeURIComponent(redirectPath); let url; if (parsePath.includes("://")) url = new URL(parsePath); else url = new URL(decodeURIComponent(redirectPath), "https://example.com"); if (action?.action === "subscribe") { let result = await subscribeToPublication(action.publication); if (result.hasFeed === false) url.searchParams.set("showSubscribeSuccess", "true"); } let path = url.pathname; if (url.search) path += url.search; if (url.hash) path += url.hash; return parsePath.includes("://") ? redirect(url.toString()) : redirect(path); };